import path from 'path';
import fs from 'fs';
import * as parse5 from "parse5";
import * as tools from '@parse5/tools'
import ejs from "ejs";
import babelParse from '@babel/parser'




/**
 * 
 * @returns {import('vite').Plugin}
 */
export function viteEjs() {

  let config = {}
  let dataRefMap = []

  // 多入口模块，虚拟模块需要各自分开
  const vModule = []


  return {
    name: 'vite-ejs',
    enforce: 'pre',

    configResolved(options) {
      config = options
      // console.log('config', config)
    },

    transformIndexHtml: {
      order: 'pre',
      handler(_, ctx) {
        const { html, styleText, scripts, dataRef } = ejsCompiler(ctx.filename, config.root, config)
        let scripts1 = ''
        if(scripts.length === 0) {
          const striptUrl = path.join(path.dirname(ctx.filename), `${path.basename(ctx.filename, '.html')}.js`)
          fs.writeFileSync(striptUrl,'','utf-8')
          scripts1 = path.relative(config.root, striptUrl)
        }
        dataRefMap = Array.from(dataRef)
        const obj = {
          entry: ctx.filename,
          indexEntry: scripts.find(s=>!!s.src)?.src || scripts1,
          full_indexEntry: '',
          jsCode: '',
          cssCode: styleText,
          jsVm: `virtual:js-module?id=${path.basename(ctx.filename).replace(/\..*/g, '')}`,
          cssVm: styleText.map((_,index)=>`virtual:css-module.css?id=${path.basename(ctx.filename).replace(/\..*/g, '')}&${index}`) 
        }
        obj.full_indexEntry = config.root + obj.indexEntry
        scripts.forEach(s=>{
          if(s.src && obj.indexEntry !== s.src) {
            obj.jsCode += `import "${s.src}";\n`
          }else if(s.content) {
            obj.jsCode += `\n;( function() { ${s.content} } )();\n`
          }
        })
        obj.jsCode += obj.cssVm.map(s=>`\nimport "${s}";`).join('\n') 
        const vm = vModule.find(m=>m.entry === ctx.filename)
        if(!vm) {
          vModule.push(obj)
        }else{
          Object.assign(vm, obj)
        }
        // console.log('vModule', vModule)
        const h = html.replace(/(?=<\/body>)/, () => {
          return `<script type="module" src="${obj.indexEntry}"></script>`
        })

        return h

      }
    },
    
    resolveId(id) {
      if (/^virtual:/.test(id)) {
        return `\0${id}`
      }
      return null
    },

    load(id) {
      if (/^\0virtual:/.test(id)) {
        for (const m of vModule) {
          if(id === `\0${m.jsVm}`) {
            return m.jsCode
          } else {
            const cssId = m.cssVm.find(css=> `\0${css}` === id)
            if(cssId) {
              return m.cssCode[id.split('&').pop()]
            }
          }
        }
      }
     
      return null
    },

    transform(code, id) {
      const vm = vModule.find(m=>m.full_indexEntry === id)
      if(vm) {
        return {
          code: code + `\nimport "${vm.jsVm}";`,
          map: this.getCombinedSourcemap()
        }
      }
    },

    handleHotUpdate(ctx) {
      if(dataRefMap.some(src=>path.normalize(src) === path.normalize(ctx.file))) {
        console.log('server restart')
        ctx.server.restart()
      } 
      
    },

  }
}

const rex = /(?<=<%-\s*include\(\s*['"]).*(?=['"]\s*\)\s*%>)/g
const rex1 = /<%-\s*include\(\s*['"].*%>/g
const rex2 = /(?<=url\(\s*['"]).*(?=['"]\s*\)\s*)/g
/**
 * 
 * @param {string} file 
 * @param {string} rootPath 
 */
export const ejsCompiler = (file, rootPath, config) => {


  /** @type {Record<string, {next: string[], template: string, file: string, pre: string, key: string}>} */
  const strMap = {}

  const fn1 = (entry, key, pre) => {
    const template = fs.readFileSync(entry, 'utf-8')
    strMap[entry] = {
      next: [],
      template: '',
      file: entry,
      pre,
      key
    }
    const emp1 = template.replace(rex1, (s) => {
      const src = s.match(rex)?.[0]
      if (src) {
        const key = `${Date.now()}_${Math.random()}`
        const dirname = path.dirname(entry)
        const file = path.resolve(dirname, src)
        strMap[entry].next.push(file)
        fn1(file, key, entry)
        return key
      } else {
        return s
      }
    })
    // const emp2 = parse(emp1)
    strMap[entry].template = emp1
    return emp1
  }

  function parse(code, file) {
    const dirname = path.dirname(file)
    const ast = parse5.parse(code)
    let params = {}
    tools.traverse(ast, {
      node(n) {
        if (n.tagName === 'style') {
          if(config.mode === 'development' && !tools.hasAttribute(n, 'data-ref')) {
            tools.setAttribute(n, 'data-ref', file)
            // n.childNodes.unshift(tools.createTextNode(`\n/* ${file} */`))
          }
          n.childNodes.forEach(i=>{
            i.value = i.value.replace(rex2, s=> {
              if(!path.isAbsolute(s)){
                const src1 = path.resolve(dirname, s)
                return src1.split(path.sep)?.join('/')
              }else{
                return s
              }
            })
          })
        }

        if (n.tagName === 'script') {
          if (n.attrs && tools.hasAttribute(n, 'aot')) {
            params = aotScript(n.childNodes[0].value, file)
            tools.removeNode(n)
          } else {
            // 
          }
        }


        if (n.attrs && tools.hasAttribute(n, 'src')) {
          const src = tools.getAttribute(n, 'src')
          if (!path.isAbsolute(src)) {
            const src1 = path.relative(rootPath, path?.join(dirname, src))
            const src2 = src1.split(path.sep)?.join('/')
            tools.setAttribute(n, 'src', '/' + src2)
          }
        }

      }
    })
    const html1 = parse5.serialize(ast).replace(/&lt\;/g, '<').replace(/&gt\;/g, '>')
    // console.log('html1', html1)
    const html2 = ejs.render(html1, params)
    return html2
  }

  function synthesis(entry) {

    if (strMap[entry].next.length > 0) {
      strMap[entry].next.forEach(f => synthesis(f))
    }
   
    const tp = strMap[entry].template
    // const html = ejs.render(tp)
    const html1 = parse(tp, entry)
    strMap[entry].template = html1
    if (strMap[entry].pre) {
      strMap[strMap[entry].pre].template = strMap[strMap[entry].pre].template.replace(strMap[entry].key, html1)
    }
  }

  function aotScript(code, entry) {
    const dirname = path.dirname(entry)
    const ast = babelParse.parse(code, { sourceType: 'module' })
   
    const result = []
    ast.program.body.forEach(node =>{
      if(node.type === 'VariableDeclaration') {
        node.declarations.forEach(n=>{
          if(n.init.type !== 'ArrowFunctionExpression') {
            result.push(n.id.name)
          }
        })
      }
    })
    const code1 = code + `\n return { ${result?.join(', ')} }`

    const fn = new Function(code1)
    let resultCode = {}
    try {
      resultCode = fn()
    } catch (error) {
      console.error(error)
    }
    return resultCode
    
  }

  fn1(file)
  synthesis(file)



  const ast = parse5.parse(strMap[file].template)
  
  const styleText = []
  const scripts = []
  const arr1 = Object.keys(strMap)
  const dataRef = new Set(arr1)

  tools.traverse(ast, {
    node(n){
      if(n.tagName === 'style') {
        let style = ''
        const ref = tools.getAttribute(n, 'data-ref')
        n.childNodes.forEach(i=>{
          if(ref) {
            style += `\n/*${ref}*/\n${i.value}`
          }else{
            style +=  `\n${i.value}`
          }
        })
        styleText.push(style)
        tools.removeNode(n)
      }
      if(n.tagName === 'script') {
        const src = tools.getAttribute(n, 'src')
        const type = tools.getAttribute(n, 'type')
        if(src) {
          scripts.push({src})
        }
        n.childNodes.forEach(i=>{
          if(i.value && type !== 'text/template') {
            scripts.push({ content: i.value})
          }
        })
        if(type !== 'text/template'){

          tools.removeNode(n)
        }

      }

    }

  })


  const html = parse5.serialize(ast)

  // console.log('html', html)

  return {
    html,
    styleText,
    scripts,
    dataRef
  }


}